/*
 *
 * Copyright (C) 2012 Walter Lütgenau
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package offset.nodes.client.veditor.model;

import java.io.IOException;
import offset.nodes.client.veditor.model.ev.DataElementViewFactory;
import offset.nodes.client.editor.model.messages.GetVirtualInformation;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DocumentFilter;
import javax.swing.text.Element;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.html.HTML;
import offset.nodes.client.virtual.model.GetSchemas;
import offset.nodes.client.virtual.model.SimpleQuery;
import offset.nodes.client.model.NodeTypes;
import offset.nodes.client.model.ServerModel;
import offset.nodes.client.model.SimpleNamespaceRegistry;
import offset.nodes.client.virtual.model.jcr.nodetype.NodeTypeDefinition;
import offset.nodes.client.virtual.model.jcr.nodetype.NodeTypeReader;
import offset.nodes.client.virtual.model.jcr.nodetype.PropertyDefinition;
import offset.nodes.client.editor.Editor;
import offset.nodes.client.editor.controller.EditorKit;
import offset.nodes.client.editor.controller.component.ComponentFocusListener;
import offset.nodes.client.editor.controller.component.ComponentKit;
import offset.nodes.client.editor.model.DocumentContext;
import offset.nodes.client.veditor.model.TreeIterator.IteratorResult;
import offset.nodes.client.veditor.model.ev.DataElementView;
import offset.nodes.client.veditor.view.ev.DataElementViewContext;
import offset.nodes.client.editor.model.messages.TemplateInformation;
import offset.nodes.client.view.component.ComponentDialogDialog;
import offset.nodes.client.view.component.ComponentDialog;
import offset.nodes.client.model.CommentedUserRequest;

/**
 * This class corresponds to a virtual block, i.e. a DIV v associated to a XSL template.
 * @author Walter Luetgenau
 */
public class VirtualBlock implements ComponentFocusListener {

    static HashMap<String, VirtualBlock> virtualBlocks = new HashMap<String, VirtualBlock>();
    String styleSheet;
    String nodeType;
    String dataPath;
    String id;
    String dataName;
    HashMap<String, Template> templatesByElementPath = new HashMap<String, Template>();
    HashMap<String, Template> templatesByName = new HashMap<String, Template>();
    VirtualElement root;
    HashMap<String, VirtualElement> dataToElement = new HashMap<String, VirtualElement>();
    TreeIterator treeIterator;
    DocumentContext context;
    HashMap properties;
    InlineProtectingDocumentFilter documentFilter;
    VirtualElement viewElement;
    Element focusElement = null;
    HashMap<String, Object> viewRegistry = new HashMap<String, Object>();

    public HashMap<String, Object> getViewRegistry() {
        return viewRegistry;
    }

    public void setViewRegistry(HashMap<String, Object> viewRegistry) {
        this.viewRegistry = viewRegistry;
    }

    public static void destroy(String page) {
        // for now, destroy information of all pages. TODO: make this page specific
        virtualBlocks = new HashMap<String, VirtualBlock>();
    }

    public String getNodeType() {
        return nodeType;
    }

    public Element getFocusElement() {
        return focusElement;
    }

    public String getStyleSheet() {
        return styleSheet;
    }

    public void setNodeType(String nodeType) {
        this.nodeType = nodeType;
    }

    public void setStyleSheet(String styleSheet) {
        this.styleSheet = styleSheet;
    }

    public DocumentContext getContext() {
        return context;
    }

    public void setContext(DocumentContext context) {
        this.context = context;
    }

    public String getDataName() {
        return dataName;
    }

    public void setDataName(String dataName) {
        this.dataName = dataName;
    }

    public String getDataPath() {
        return dataPath;
    }

    public void setDataPath(String dataPath) {
        this.dataPath = dataPath;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public VirtualElement getRoot() {
        return root;
    }

    public void setRoot(VirtualElement root) {
        this.root = root;
    }

    public Template getTemplateByName(String name) {
        return templatesByName.get(name);
    }

    public Template getTemplateByElementPath(String elementPath) {
        return templatesByElementPath.get(elementPath);
    }

    protected boolean mayOccurMultiply(VirtualElement v) {
        if (v.getName().equals(HTML.Tag.TR.toString()) ||
                v.getName().equals(HTML.Tag.LI.toString()))
            return true;
        return false;
    }

    protected String nameToPath(String name) {
        return "/" + name;
    }

    protected VirtualElement cloneAndResolve(VirtualElement e) throws Exception {
        VirtualElement clone = (VirtualElement) e.clone();

        for (int i = 0; i < e.getElementCount(); i++) {
            VirtualElement child = (VirtualElement) e.getElement(i);
            if (child.isDataElement()) {
                Template template = templatesByName.get(child.getName());
                VirtualElement childRoot = template.getRoot();
                if (template.occursMultiple() )
                    childRoot.setMultiple(true);
                else if (nameToPath(child.getName()).startsWith(DataModel.SECONDARY_QUERY) && mayOccurMultiply(childRoot)) {
                    String secondaryQuery = child.getName().substring(0, child.getName().indexOf("."));
                    SimpleQuery query = getModel().getSecondaryQuery(secondaryQuery);
                    if (query.getReferenceDirection() == SimpleQuery.REFERENCE_FROM_THIS) {
                        TemplateInformation templateInformation = templatesByElementPath.get(getNodeType()).getTypeInformation();
                        for (PropertyDefinition propertyDefinition : templateInformation.getNodeTypeDefinition().getDeclaredPropertyDefinitions()) {
                            if (SimpleNamespaceRegistry.getInstance().toPrefixName(propertyDefinition.getName()).equals(query.getReferenceProperty())) {
                                childRoot.setMultiple(propertyDefinition.isMultiple());
                                break;
                            }
                        }
                    } else
                        childRoot.setMultiple(true);
                }
                childRoot.setContent(template.getDescriptionText());
                childRoot.setTypeInformation(template.getTypeInformation());
                VirtualElement clonedChild = cloneAndResolve(childRoot);
                clonedChild.setDataElement(true);
                if (!getModel().isData(clonedChild))
                    getModel().addAttribute(clonedChild, DataModel.ATTRIBUTE_NAME_DATA, template.getElementPath());
                clone.addChild(clonedChild);
            } else
                clone.addChild(cloneAndResolve(child));
        }

        return clone;
    }

    /**
     * Return the virtual element corresponding to the given real element e.
     *
     * @param e
     * @return The corresponding virtual element, if found. Else null.
     */
    VirtualElement getCorrespondingVirtualElement(Element e) {
        if (e.getAttributes().isDefined(DataModel.ATTRIBUTE_NAME_DATA))
            return dataToElement.get((String) e.getAttributes().getAttribute(DataModel.ATTRIBUTE_NAME_DATA));

        Stack<Element> path = new Stack<Element>();
        while (e != null && !e.getAttributes().isDefined(DataModel.ATTRIBUTE_NAME_TEMPLATE)) {
            path.push(e);
            e = e.getParentElement();
        }

        return getCorrespondingVirtualElement((Stack<Element>) path.clone(), root);
    }

    /**
     * Return the virtual element corresponding to the real element path.
     *
     * @param path
     * @param root The root of the virtual elements
     * @return The corresponding virtual element, if found. Else null
     */
    VirtualElement getCorrespondingVirtualElement(Stack<Element> path, VirtualElement root) {
        if (path.size() == 0)
            return root;

        VirtualElement v = root;
        VirtualElement found = null;
        DataModel model = getModel();

        while (path.size() > 0) {
            Element pathElement = path.pop();

            for (int i = 0; i < v.getElementCount(); i++) {
                found = null;

                if (v.getVirtualElement(i).equals(pathElement)) {
                    if (!model.hasData(v.getElement(i))) {
                        // no data element. In this case, identification depends on the data children.
                        /* if this is the leaf of the path, but the virtual element has children, check, if the two elements are equivalent, i.e. have same data child elements */
                        if (path.size() == 0 && v.getVirtualElement(i).getElementCount() > 0 && !isEquivalent(v.getVirtualElement(i), pathElement))
                            continue;
                        found = getCorrespondingVirtualElement((Stack<Element>) path.clone(), v.getVirtualElement(i));
                        if (found != null)
                            return found;
                        continue;
                    } else // different data element
                    if (!model.hasData(pathElement) || !model.getData(v.getVirtualElement(i)).equals(model.getData(pathElement)))
                        continue;

                    // same data element
                    found = v.getVirtualElement(i);
                }

                if (found != null) {
                    v = found;
                    break;
                }
            }

            if (found == null)
                return null;

        }

        return found;
    }

    /**
     * Find multipleIndex of the given virtual v
     * 
     * @param v the virtual v with the requested multipleIndex
     * @param e the real v
     * @return the result of the check
     */
    protected int getMultipleIndex(VirtualElement v, Element e) {
        if (!v.isMultiple())
            return -1;

        int multipleCount = 0;
        Element parentElement = e.getParentElement();
        for (int i = getModel().getChildIndex(e); i >= 0; i--) {
            e = parentElement.getElement(i);
            if (!v.equals(e))
                break;
            multipleCount++;
        }

        return multipleCount - 1;
    }

    /**
     * Return the last multiple element
     * 
     * @param e the real v
     * @return the result of the check
     */
    protected Element getLastMultipleElement(VirtualElement v, Element e) {
        Element parentElement = e.getParentElement();
        int start = getModel().getChildIndex(e);
        Element before = null;
        for (int i = start; i < parentElement.getElementCount(); i++) {
            Element child = parentElement.getElement(i);
            if (!v.equals(child))
                break;
            before = child;
        }

        return before;
    }

    /**
     * Is the element the last multiple element of the type v?
     * 
     * @param v the virtual v, used to determine the type
     * @param e the real element
     * @return the result of the check
     */
    protected boolean isLastMultipleIndex(VirtualElement v, Element e) {
        if (!v.isMultiple() || !v.equals(e))
            return false;

        int childIndex = getModel().getChildIndex(e);
        if (e.getParentElement().getElementCount() > childIndex + 1 && v.equals(e.getParentElement().getElement(childIndex + 1)))
            return false;

        return true;
    }

    /**
     * Create a VirtualElementInstance from a VirtualElement and its corresponding Element. All parents of the 
     * result are VirtualElementInstances
     * 
     * @param v the VirtualElement
     * @param e the corresponding Element
     * @return the VirtualElementInstance.
     */
    VirtualElementInstance getVirtualElementInstance(VirtualElement v, Element e) {
        VirtualElementInstance result = null;
        VirtualElementInstance current = null;
        VirtualElementInstance child = null;

        while (v != null) {
            current = new VirtualElementInstance(v, this);
            if (result == null)
                result = current;

            if (v.isMultiple())
                current.setMultipleIndex(getMultipleIndex(v, e));
            if (isLastMultipleIndex(v, e))
                current.setLast(true);

            if (child != null)
                child.setParentInstance(current);
            child = current;

            v = v.getParent();
            e = e.getParentElement();
        }

        return result;
    }

    VirtualElementInstance getCorrespondingVirtualElementInstance(Element e) {
        return getVirtualElementInstance(getCorrespondingVirtualElement(e), e);
    }

    Element getRootElement(Element e) {
        if (e.getAttributes().isDefined(DataModel.ATTRIBUTE_NAME_TEMPLATE) &&
                e.getAttributes().getAttribute(DataModel.ATTRIBUTE_NAME_TEMPLATE).equals(getStyleSheet()))
            return e;

        for (int i = 0; i < e.getElementCount(); i++) {
            Element found = getRootElement(e.getElement(i));
            if (found != null)
                return found;
        }

        return null;
    }

    Element getRootElement() {
        Element documentRoot = getModel().getDocument().getRootElements()[0];
        return getRootElement(documentRoot);
    }

    /**
     * Find the v corresponding to the virtual v
     *
     * @param root the root v
     * @param v the virtual v, the corresponding v of which shall be found
     * @param last if multiple elements match, find the last
     * @return the corresponding v
     */
    Element getCorrespondingElement(VirtualElementInstance v) {
        Element root = getRootElement();
        if (v.equals(root))
            return root;

        Stack<VirtualElementInstance> path = new Stack<VirtualElementInstance>();
        while (v != null && v.getParentInstance() != null) {
            path.push(v);
            v = v.getParentInstance();
        }

        return getCorrespondingElement(path, root);
    }

    /**
     * Find the element starting from root corresponding to the virtual path.
     *
     * @param path The path starting from root to the virtual element, for which a corresponding real element shall be found.
     * @param root The root of the real elements.
     * @return the corresponding real element, if found. Else null.
     */
    Element getCorrespondingElement(Stack<VirtualElementInstance> path, Element root) {
        if (path.size() == 0)
            return root;

        Element e = root;
        Element found = null;

        DataModel model = getModel();
        while (path.size() > 0) {
            VirtualElementInstance pathInstance = path.pop();
            for (int i = 0; i < e.getElementCount(); i++) {
                found = null;

                if (pathInstance.equals(e.getElement(i))) {
                    if (!model.hasData(e.getElement(i))) {
                        // no data element
                        found = getCorrespondingElement((Stack<VirtualElementInstance>) path.clone(), e.getElement(i));
                        if (found != null)
                            return found;
                        continue;
                    } else // different data element
                    if (!model.hasData(pathInstance) || !model.getData(e.getElement(i)).equals(model.getData(pathInstance)))
                        continue;

                    // same data element
                    if (pathInstance.isMultiple()) {
                        if (pathInstance.getMultipleIndex() == VirtualElementInstance.MI_LAST)
                            found = getLastMultipleElement(pathInstance, e.getElement(i));
                        else 
                            found = e.getElement(i);
                    } else
                        found = e.getElement(i);
                }

                if (found != null) {
                    e = found;
                    break;
                }
            }

            if (found == null)
                return null;
        }

        return found;
    }

    public DataModel getModel() {
        return new DataModel(context, properties);
    }

    public void setContext(DocumentContext context, HashMap properties) {
        this.context = context;
        this.properties = properties;
    }

    protected void init(String name) throws Exception {
        root = templatesByElementPath.get(name).getRoot();
        root.setDataElement(true);
        root.setIsLeaf(false);
        root = cloneAndResolve(root);
        ((SimpleAttributeSet) root.getAttributes()).addAttribute(DataModel.ATTRIBUTE_NAME_TEMPLATE, getStyleSheet());
        
        treeIterator = new TreeIterator(root, getModel(), this);
        documentFilter = new InlineProtectingDocumentFilter();
        getModel().getDocument().setDocumentFilter(documentFilter);
      
        ComponentKit componentKit = ((EditorKit) getEditor().getBrowserPane().getEditorKit()).getComponentKit();
        componentKit.addComponentFocusListener(this);
    }

    @Override
    public void focusGained(Element componentElement) {
        try {
            int before = 0;
            if (focusElement != null)
                before = focusElement.getStartOffset();
            
            moveCaret(before, componentElement.getStartOffset());
            
        } catch (Exception ex) {
            Logger.getLogger(VirtualBlock.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
    
    

    /**
     * This document filter serves multiple purposes:
     * 1. It will protect inline elements, in particular hyperlink inline elements. These will be destroyed by the default action (replace).
     * 2. If a edit dialog shall be used, it can be used to ignore all user input, except CR, which will start the dialog.
     */
    class InlineProtectingDocumentFilter extends DocumentFilter {

        boolean ignoreInput = false;

        public void setIgnoreInput(boolean ignoreInput) {
            this.ignoreInput = ignoreInput;
        }

        public void replace(FilterBypass fb, int offset, int length, String text,
                AttributeSet attrs) throws BadLocationException {
            if (ignoreInput)
                return;

            Element e = getModel().getDocument().getParagraphElement(offset);
            if (getModel().isArtificialWithInlineElement(e)) {
                fb.insertString(offset, text, attrs);
                fb.remove(offset + text.length(), length);
            } else
                fb.replace(offset, length, text, attrs);
        }
    }

    /**
     * Return the editor. The editor will be used as a global wrapper structure, among others to centralize caches.
     * @return The editor
     */
    public Editor getEditor() {
        return Editor.getEditor(getContext().getPane());
    }

    /**
     * Init the NodeTypes. These will not be necessary for the ordinary function of the VirtualBlock, hence they will
     * only be initialized on demand
     * @return The NodeTypes
     */
    protected NodeTypes initNodeTypes() {
        GetSchemas.Request request = new GetSchemas.Request();
        GetSchemas.Response response = null;
        try {
            String serverUrl = (String) getEditor().getProperties().get(Editor.PROP_UPLOAD_TO);
            ServerModel server = new ServerModel(new URL(serverUrl));
            response = (GetSchemas.Response) server.sendRequest(request);
            InputStream in = new ByteArrayInputStream(response.getSchemaXml().getBytes());
            NodeTypeDefinition[] definitions = NodeTypeReader.read(in);

            return new NodeTypes(definitions);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }

    }

    /**
     * Return the NodeTypes. If necessary, initialize them and update the central cache in Editor.
     * @return The NodeTypes
     */
    public NodeTypes getNodeTypes() {
        NodeTypes nodeTypes = (NodeTypes) getEditor().getProperties().get(Editor.PROP_NODE_TYPES); 
        if (nodeTypes == null)
            nodeTypes = initNodeTypes();

        return nodeTypes;

    }

    /**
     * Return the central cache of virtual blocks. It will be on an Editor, i.e. a per page, basis.
     * @param context
     * @return
     */
    protected static synchronized HashMap<String, VirtualBlock> getVirtualBlocks(DocumentContext context) {
        HashMap<String, VirtualBlock> virtualBlocks = (HashMap<String, VirtualBlock>) Editor.getEditor(context.getPane()).getProperties().get(Editor.PROP_VIRTUAL_BLOCKS); 
        if (virtualBlocks == null) {
            virtualBlocks = new HashMap<String, VirtualBlock>();
            Editor.getEditor(context.getPane()).getProperties().put(Editor.PROP_VIRTUAL_BLOCKS, virtualBlocks); 
        }

        return virtualBlocks;
    }

    /**
     * Return the correct virtual block from the cache.
     * @param styleSheetPath
     * @param model
     * @return
     * @throws java.lang.Exception
     */
    public static VirtualBlock getVirtualBlock(String styleSheetPath, DataModel model) throws Exception {
        Element virtualBlock = model.getTemplateElement(styleSheetPath);
        return getVirtualBlock(virtualBlock, model);
    }

    /**
     * Return a virtual block, initialize it, if it is not already in the cache.
     * @param e
     * @param model
     * @return
     * @throws java.lang.Exception
     */
    public static VirtualBlock getVirtualBlock(Element e, DataModel model) throws Exception {
        String styleSheetPath = (String) e.getAttributes().getAttribute(DataModel.ATTRIBUTE_NAME_TEMPLATE);
        if (styleSheetPath == null)
            return null;

        VirtualBlock virtualBlock = getVirtualBlocks(model.getContext()).get(styleSheetPath);
        if (virtualBlock != null)
            return virtualBlock;

        while (!e.getAttributes().isDefined(DataModel.ATTRIBUTE_NAME_TEMPLATE) && e.getParentElement() != null) {
            e = e.getParentElement();
        }

        if (e == null)
            return null;

        Element virtualBlockElement = e;
        String nodeType = (String) virtualBlockElement.getAttributes().getAttribute(DataModel.ATTRIBUTE_NAME_SCHEMA);

        GetVirtualInformation.Response response = model.readVirtualInformation(styleSheetPath, nodeType);

        virtualBlock = new VirtualBlock();

        String styleSheet = (String) virtualBlockElement.getAttributes().getAttribute(DataModel.ATTRIBUTE_NAME_TEMPLATE);
        String dataName = model.getElementName(virtualBlockElement); //(String) virtualBlockElement.getAttributes().getAttribute(DataModel.ATTRIBUTE_NAME_DATA);
        String id = (String) virtualBlockElement.getAttributes().getAttribute(DataModel.ATTRIBUTE_NAME_ID);
        String dataPath = (String) virtualBlockElement.getAttributes().getAttribute(DataModel.ATTRIBUTE_NAME_PATH);

        virtualBlock.setDataName(dataName);
        virtualBlock.setDataPath(dataPath);
        virtualBlock.setId(id);
        virtualBlock.setStyleSheet(styleSheet);
        virtualBlock.setNodeType(nodeType);

        StyleSheetReader reader = new StyleSheetReader(virtualBlock);
        HashMap<String, Template> templatesByElementPath = reader.readStyleSheet(new ByteArrayInputStream(response.getTransformation().getBytes()), response.getNodeTypeInformation());
        HashMap<String, Template> templatesByName = new HashMap<String, Template>();
        Iterator<Template> i = templatesByElementPath.values().iterator();
        while (i.hasNext()) {
            Template template = i.next();
            if (template.getName() == null || template.getName().length() == 0)
                template.setName(dataName);
            templatesByName.put(template.getName(), template);
        }


        virtualBlock.templatesByName = templatesByName;
        virtualBlock.templatesByElementPath = templatesByElementPath;
        virtualBlock.setContext(model.getContext(), model.getProperties());

        virtualBlock.init(dataName);
        getVirtualBlocks(model.getContext()).put(styleSheetPath, virtualBlock);

        return virtualBlock;
    }

    int getLevel(Element e) {
        int level = 0;
        while (e != null && !e.getAttributes().isDefined(DataModel.ATTRIBUTE_NAME_TEMPLATE)) {
            e = e.getParentElement();
            level++;
        }
        return level;
    }

    VirtualElement getLeaf(VirtualElement parent, boolean forward) {
        if (parent.isLeaf())
            return parent;

        if (forward)
            return getLeaf(parent.getVirtualElement(0), forward);
        else
            return getLeaf(parent.getVirtualElement(parent.getElementCount() - 1), forward);
    }

    boolean insertAllChildren(VirtualElement v) {
        return v.getName().equalsIgnoreCase("tr");
    }

    boolean cannotHighlight(Element v) {
        return getModel().isArtificialWithInlineElement(v);
    }

    boolean cannotHighlight(VirtualElement v) {
        String[] names = {"a", "span"};

        for (int i = 0; i < names.length; i++) {
            if (v.getName().equalsIgnoreCase(names[i]))
                return true;
        }
        return false;
    }

    protected void setViewElement(VirtualElement v) {
        documentFilter.setIgnoreInput(true);
        this.viewElement = v;
    }

    protected void resetViewElement() {
        documentFilter.setIgnoreInput(false);
        this.viewElement = null;
    }

    public Element showView() {
        if (this.viewElement == null)
            return null;
        
        DataElementView elementView = (DataElementView) DataElementViewFactory.getView(this.viewElement);

        int pos = getModel().getContext().getPane().getCaretPosition();

        Element dataElement = getModel().getDataElement(pos);
        Element focusElement = dataElement;
        if (getModel().hasInlineDataElement(dataElement))
            focusElement = dataElement.getParentElement();
        VirtualElement v = getCorrespondingVirtualElement(focusElement);
        VirtualElementInstance vi = getVirtualElementInstance(v, focusElement);

        elementView.setContext(new DataElementViewContext(this, dataElement));

        Element e = getCorrespondingElement(vi);
        return e;
    }

    /**
     * Return the content of the child element. If it is part of a binary,
     * return the inner HTML of the child element.
     *
     * @param model
     * @param child
     * @param childElement
     * @return
     * @throws BadLocationException
     * @throws IOException
     */
    protected String getContent(DataModel model, VirtualElement child, Element childElement) throws BadLocationException, IOException {
        if (child.isBinary() || (child.getElementCount() == 1 && ((VirtualElement) child.getElement(0)).isBinary()))
            return model.getInnerHTML(childElement);
        else
            return model.getContent(childElement);
    }

    protected boolean consumeChildForExistingElement(StringBuffer buf, DataModel model, VirtualElement child, Element childElement, IteratorResult toInsert,
            Element toRemove, Element before, boolean forward) throws Exception {
        int childIndex = model.getChildIndex(childElement);
        if (childElement != toRemove) { // only add, if not to remove

            if (child.isLeaf()) {
                /**
                 * Do only add a focus decoration, when the element is the last element and it has not yet been inserted. The latter might happen when
                 * traversing backwards.
                 */
                if (toInsert.getHighlight() != null
                        && hasSamePath(toInsert.getHighlight(), child)
                        && toInsert.contains(child, -1, childElement.getParentElement())
                        && hasSamePath(toInsert.getHighlight(), childElement)) {
                    if (DataElementViewFactory.hasElementView(toInsert.getLast())) {
                        buf.append(model.getStartTag(child, childElement, HighlightAttributes.ATT_VIEW) + getContent(model, child, childElement) + model.getEndTag(child));
                        setViewElement(toInsert.getLast());
                    } else
                        buf.append(model.getStartTag(child, childElement, HighlightAttributes.ATT_EDIT) + model.getContent(childElement) + model.getEndTag(child));
                    toInsert.consumeHighlight();
                } else {
                    buf.append(model.getStartTag(child, childElement) + getContent(model, child, childElement) + model.getEndTag(child));
                }
            }
            else
                buf.append(insertAndRemove(model, child, childElement, toInsert, toRemove, before, forward));

            if (toInsert.contains(child, childIndex, childElement.getParentElement()))
                toInsert.remove(child);


        }

        // if there is an existing multiple v of the same schema v after this v, don't consume the schema v
        if (child.isMultiple() && childIndex + 1 < childElement.getParentElement().getElementCount() && child.equals(childElement.getParentElement().getElement(childIndex + 1)))
            return false;

        return true;
    }

    protected boolean consumeChildForNewElement(StringBuffer buf, DataModel model, VirtualElement child, Element childElement, IteratorResult toInsert, Element toRemove,
            Element before, boolean forward) throws Exception {
        buf.append(insertAndRemove(model, child, null, toInsert, toRemove, before, forward));
        toInsert.remove(child); // remove inserted v from the list      

        // if there is an existing multiple v of the same schema v after the v added newly, don't consume the schema v
        if (child.isMultiple() && childElement != null && child.equals(childElement))
            return false;

        return true;
    }

    /**
     * Is this a row selector? A row selector is a TD, that has a single
     * child A, which is editable. This will select the hyperlink and with this the content
     * of the row.
     *
     * @param v the virtual element in question
     * @return
     */
    public boolean isRowSelector(VirtualElementInstance v) {
        if (!v.getName().equals(HTML.Tag.TD.toString()))
            return false;
        if (v.getElementCount() != 1)
            return false;
        VirtualElement child = v.getVirtualElement(0);
        if (!child.getName().equals(HTML.Tag.A.toString()))
            return false;
        String editable = (String) child.getAttributes().getAttribute(DataModel.ATTRIBUTE_NAME_EDIT);
        return DataModel.ATTRIBUTE_VALUE_TRUE.equals(editable);
    }

    protected boolean isDescendant(Element ancestor, Element descendent) {
        if (ancestor == null || descendent == null)
            return false;

        while (descendent != null) {
            if (ancestor == descendent)
                return true;

            descendent = descendent.getParentElement();
        }

        return false;
    }

    /**
     * Is the virtual element equivalent to the real element?
     * The real element is considered equivalent to the virtual element, if there is a possible counterpart
     * for the element and for each element in the subtree of the element in
     * the virtual element and its subtree.
     *
     * A virtual element is considered a possible counterpart, if the VirtualElement.equals()
     * method does not object:
     * 
     * 1. Data elements must have the same data association,
     * 2. non data leafs must have the same content and the same name,
     * 3. non data branch elements must have the same name.
     *
     * A special case is an element marked as read only. In that case, the content is always equivalent
     *
     * 1. if both element and virtual element are marked as readonly
     * 2. if both element and virtual element are data elements associated to the same data.
     *
     * @param v the virtual element
     * @param e the element
     * @return
     */
    protected boolean isEquivalent(VirtualElement v, Element e) {
        if (!v.equals(e))
            return false;

        if (v.isLeaf())
            return true;

        if (getModel().isReadOnly(e) && getModel().isReadOnly(v) && v.isDataElement() && e.getAttributes().isDefined(DataModel.ATTRIBUTE_NAME_DATA))
            return true; // data association must be the same, because "equals" succeeded

        e = getModel().getHtmlElement(e);
        for (int i = 0; i < e.getElementCount(); i++ ) {
            Element child = e.getElement(i);

            boolean equivalent = false;
            for (int j = 0; j < v.getElementCount(); j++) {
                if (v.getVirtualElement(j).equals(child)) {
                    equivalent = true;
                    break;
                }
            }

            if (!equivalent)
                return false;
        }

        return true;
    }

    /**
     * Return the value of the id attribute of the element, if there is any.
     * 
     * @param e The element
     * @return The value of the id attribute
     */
    protected String getId(Element e) {
        if (e == null || !e.getAttributes().isDefined(DataModel.ATTRIBUTE_NAME_ID))
            return null;
        String id = (String) e.getAttributes().getAttribute(DataModel.ATTRIBUTE_NAME_ID);
        if (id.startsWith("{")) // only placeholder present
            return null;

        return id;
    }

    /**
     * Return the value of the primary reference attribute of the element, if there is any.
     * 
     * @param e The element
     * @return The value of the id attribute
     */
    protected String getPrimaryReference(Element e) {
        if (e == null || !e.getAttributes().isDefined(DataModel.ATTRIBUTE_NAME_PRIMARY_REFERENCE))
            return null;
        String reference = (String) e.getAttributes().getAttribute(DataModel.ATTRIBUTE_NAME_PRIMARY_REFERENCE);

        return reference;
    }

    /**
     * Return the start tag created from the attribute set of the virtual element. Substitute the passed attribute
     * with the value provided temporarily. Return the start tag resulting from that substitution.
     * 
     * @param model The data model
     * @param ve The virtual element
     * @param substituteKey The key of the attribute to temporarily substitute
     * @param substituteValue The value to be substituted temporarily
     * @return The start tag resulting from the substitution
     */
    protected String getStartTag(DataModel model, VirtualElement ve, String substituteKey, String substituteValue) {
        // create modified attribute set
        SimpleAttributeSet atts = new SimpleAttributeSet(ve.getAttributes());
        atts.removeAttribute(substituteKey);
        atts.addAttribute(substituteKey, substituteValue);

        // backup old attribute set
        SimpleAttributeSet old = ve.getAttributes();

        ve.setAttributes(atts); // apply new set
        String startTag = model.getStartTag(ve);
        ve.setAttributes(old); // restore old set

        return startTag;

    }

    protected String insertAndRemove(DataModel model, VirtualElement parent, Element parentElement, IteratorResult toInsert, Element toRemove, Element before, boolean forward) throws Exception {
        StringBuffer buf = new StringBuffer();

        // take over some attributes not present in the template, but in the current element of the pane
        SimpleAttributeSet atts = new SimpleAttributeSet(parent.getAttributes());
        String parentId = getId(parentElement);
        if (parentId != null)
            atts.addAttribute(DataModel.ATTRIBUTE_NAME_ID, parentId);
        String primaryReference = getPrimaryReference(parentElement);
        if (primaryReference != null)
            atts.addAttribute(DataModel.ATTRIBUTE_NAME_PRIMARY_REFERENCE, primaryReference);

        if (toInsert.getHighlight() != null && hasSamePath(toInsert.getHighlight(), parent) && hasSamePath(toInsert.getHighlight(), parentElement)) {
            if (DataElementViewFactory.hasElementView(toInsert.getLast())) {
                buf.append(model.getStartTag(atts, HighlightAttributes.ATT_VIEW));
                setViewElement(toInsert.getLast());
            } else
                buf.append(model.getStartTag(atts, HighlightAttributes.ATT_EDIT));
            toInsert.consumeHighlight();
        } else
            buf.append(model.getStartTag(atts));

        for (int i = 0, j = 0; i < parent.getElementCount();) {
            Element childElement = null;
            VirtualElement child = parent.getVirtualElement(i);
            if (parentElement != null && j < parentElement.getElementCount())
                childElement = parentElement.getElement(j);

            /* consider the order of existing and new v depending on the tab direction */
            boolean childConsumed = false;

            if (child.isBinary() && !model.canEdit(child)) { // element was a placeholder only. Copy all existing elements.
                if (childElement != null) {
                    j++;
                    buf.append(model.getOuterHTML(childElement));
                }
                else
                    childConsumed = true;
            }
            else if (childElement != null && isEquivalent(child, childElement)) // child already present
                /* if both insert and existing at the same position and it is backward direction, 
                 * the situation depends on whether the cursor was within the element. 
                 * Only if this is true insert a new element before the existing element.
                 */
                if (!forward && child.isMultiple() && toInsert.contains(child, -1, parentElement) && isDescendant(childElement, before))
                    childConsumed = consumeChildForNewElement(buf, model, child, childElement, toInsert, toRemove, before, forward);
                else {
                    j++;
                    childConsumed = consumeChildForExistingElement(buf, model, child, childElement, toInsert, toRemove, before, forward);
                }
            else if (toInsert.contains(child, -1, parentElement)) // child shall be inserted
                childConsumed = consumeChildForNewElement(buf, model, child, childElement, toInsert, toRemove, before, forward);
            else
                childConsumed = true;

            if (!childConsumed || (child.isMultiple() && toInsert.contains(child, -1, parentElement))) // if it is multiple and has not been consumed above
                continue;

            i++;
        }

        if (parent.isLeaf() && parent.getContent() != null)
            buf.append(parent.getContent());

        buf.append(model.getEndTag(parent));

        return buf.toString();
    }

    protected Element getRemovableRoot(Element removableElement, VirtualElement removable, boolean forward) throws Exception {
        boolean isEmpty = true;
        Element parentElement = removableElement.getParentElement();
        for (int i = 0; i < parentElement.getElementCount(); i++) {
            Element child = parentElement.getElement(i);
            if (removableElement != child && hasNonDefaultData(child))
                isEmpty = false;
        }

        if (!isEmpty || getModel().isTemplateElement(parentElement) || !isLastChild(removable, forward))
            return (removableElement);
        else
            return getRemovableRoot(parentElement, removable.getParent(), forward);
    }

    /**
     * A element has the same multiple path only, if all multiply occuring elements in the path
     * have the same multiple index.
     * 
     * @param vi The virtual element instance to be compared against
     * @param e The real element
     * @return do they have the same multiple path?
     */
    protected boolean hasSamePath(VirtualElementInstance vi, Element e) {
        while (vi != null && e != null) {
            if (!vi.equals(e))
                return false;
            vi = vi.getParentInstance();
            e = e.getParentElement();
        }

        return true;
    }

    protected boolean hasNonDefaultData(Element element) throws Exception {
        DataModel model = getModel();

        if (model.isComponent(element))
            return true;
        
        if (element.getEndOffset() - element.getStartOffset() == 1)
            return false;

        if (model.isBinary(element)) // binary with data, as the size is greater than 1, see above
            return true;

        if (model.hasData(element))
            if (model.isLeaf(element))
                if (model.onlyDescriptionText(element))
                    return false;
                else
                    return true;

        for (int i = 0; i < element.getElementCount(); i++) {
            // TODO: recognize non-data elements with data descendent
            Element child = element.getElement(i);
            if (hasNonDefaultData(child))
                return true;
        }

        return false;
    }

    protected boolean hasData(VirtualElement v) throws Exception {
        if (v.isDataElement())
            return true;

        for (int i = 0; i < v.getElementCount(); i++) {
            // TODO: recognize non-data elements with data descendent
            VirtualElement child = v.getVirtualElement(i);
            if (hasData(child))
                return true;
        }

        return false;
    }

    /**
     * Considers the "insertAllChildren" condition. This is true, e.g., for a table row. Do not insert just the first cell, which is visually
     * annoying, but all cells.
     * 
     * Consequently, the cells may be removed while traversing only when leaving the last cell in the traverse direction.
     * 
     * @param v
     * @param forward
     * @return
     */
    protected boolean canRemove(VirtualElement v, Element e, boolean forward) throws Exception {
        if (v.getName().equals(HTML.Tag.A.toString()))
            v = v.getParent();
        if (!insertAllChildren(v.getParent()))
            return true;

        int index = getModel().getChildIndex(v);
        if (forward) {
            // are there any elements before, that already have data?
            for (int i = 0; i < index; i++) {
                if (hasNonDefaultData(e.getParentElement().getElement(i)))
                    return false;
            }
            
            // are there any elements afterwards, that are associated to data?
            if (index < v.getParent().getElementCount() - 1) {
                for (int i = index + 1; i < v.getParent().getElementCount(); i++) {
                    VirtualElement sibling = v.getParent().getVirtualElement(i);
                    if (sibling.isDataElement() && getModel().canEdit(sibling)) {
                        return false;
                    }
                }
            }
        } else if (!forward) {
            // are there any elements afterwards, that already have data?
            for (int i = e.getParentElement().getElementCount() - 1; i > index; i--)
                if (hasNonDefaultData(e.getParentElement().getElement(i)))
                    return false;
            
            // are there any elements before, that are associated to data?
            if (index > 0) {
                for (int i = index - 1; i >= 0; i--) {
                    VirtualElement sibling = v.getParent().getVirtualElement(i);
                    if (sibling.isDataElement() && getModel().canEdit(sibling)) {
                        return false;
                    }
                }
            }
        }

        return true;
    }

    /**
     * Only delete the parentElement tree as well, if it is the last Child of a sequence. Else, traverse the remaining children of the
     * sequence first 
     * 
     * @param v the current v
     * @param forward the direction of the traversal
     * @return is it the last child regarding the direction?
     */
    protected boolean isLastChild(VirtualElement v, boolean forward) throws Exception {
        int index = getModel().getChildIndex(v);
        if (forward && index < v.getParent().getElementCount() - 1 && hasData(v.getParent().getVirtualElement(index + 1)) && getModel().canEdit(v.getParent().getVirtualElement(index + 1)))
            return false;
        else if (!forward && index > 0 && hasData(v.getParent().getVirtualElement(index - 1)))
            return false;

        return true;
    }

    /**
     * Return the element with the highlight attributes.
     * Search the element passed and all its children
     * 
     * @param e
     * @return
     */
    protected Element getHighlightedElement(Element e) {
        if (HighlightAttributes.isHighlight(e.getAttributes()))
            return e;

        for (int i = 0; i < e.getElementCount(); i++) {
            Element highlight = getHighlightedElement(e.getElement(i));
            if (highlight != null)
                return highlight;
        }

        return null;
    }

    /**
     * If the existing data v at pos is empty, remove it and all parentElement elements, that get empty by deleting the specific v as well.
     * Determine the next virtual data v after pos (skipping the v, that will be removed).
     * 
     * Determine the common parentElement of the v toRemove  and the v toInsert and do both operations in a single call to setOuterHTML.
     * Determine the leaf v, that has been inserted and return it. This will give the action an opportunity to select the corresponding v.
     * 
     * @param pos the current position
     * @param forward true, if the next data v shall be inserted, else false
     * @return the leaf v, that has been there before or has been inserted
     * 
     * @throws javax.swing.text.BadLocationException
     * @throws java.io.IOException
     */
    public Element successorDataElement(int pos, boolean forward) throws Exception {
        resetViewElement();

        DataModel model = getModel();

        Element beforeElement = model.getDataElement(pos);
        VirtualElement before = getCorrespondingVirtualElement(beforeElement);
        Element toRemove = null;
        if (!hasNonDefaultData(beforeElement) && canRemove(before, beforeElement, forward)) {
            toRemove = getRemovableRoot(beforeElement, before, forward);
            beforeElement = toRemove;
            before = getCorrespondingVirtualElement(beforeElement);
        }

        IteratorResult toInsert = treeIterator.successorDataElement(beforeElement, toRemove, forward);

        // determine real common parent element from virtual common parent element
        int up = before.getLevel() - toInsert.getCommonParent().getLevel();
        Element parentElement = beforeElement;
        for (int i = 0; i < up; i++) {
            parentElement = parentElement.getParentElement();
        }

        // add new v
        VirtualElementInstance leaf = toInsert.getLast();
        model.setOuterHTML(parentElement, insertAndRemove(model, toInsert.getCommonParent(), parentElement, toInsert, toRemove, beforeElement, forward));

        // determine leaf v for selection
        Element leafElement = getHighlightedElement(getRootElement()); // getCorrespondingElement(leaf);

        focusElement = leafElement;

        return leafElement;
    }

    public Element nextDataElement(int pos) throws Exception {
        return successorDataElement(pos, true);
    }

    public Element previousDataElement(int pos) throws Exception {
        return successorDataElement(pos, false);
    }

    /**
     * Modify the element attributes when changing the focus from an old data element to a new data element
     * 
     * @param parent The parent element
     * @param before The leaf data element, that had the focus before
     * @param now The leaf data element, that will have the focus now
     * @return The HTML string representing the modified elements
     * 
     * @throws java.lang.Exception
     */
    protected String changeFocus(Element parent, Element remove, Element before, Element now) throws Exception {
        StringBuffer buf = new StringBuffer();

        buf.append(getModel().getStartTag(parent));

        for (int i = 0; i < parent.getElementCount(); i++) {
            Element child = parent.getElement(i);

            if (child == remove)
                continue;

            if (child == now || (cannotHighlight(now) && child == now.getParentElement())) {
                VirtualElement v = getCorrespondingVirtualElement(child);
                VirtualElement vChild = null;
                if (v.getElementCount() > 0)
                    vChild = v.getVirtualElement(0);
                if (DataElementViewFactory.hasElementView(v)) {
                    buf.append(getModel().getStartTag(v, HighlightAttributes.ATT_VIEW) + getModel().getInnerHTML(child) + getModel().getEndTag(v));
                    setViewElement(v);
                } else if (vChild != null && DataElementViewFactory.hasElementView(vChild)) {
                    buf.append(getModel().getStartTag(v, HighlightAttributes.ATT_VIEW) + getModel().getInnerHTML(child) + getModel().getEndTag(v));
                    setViewElement(vChild);
                } else
                    buf.append(getModel().getStartTag(v, child, HighlightAttributes.ATT_EDIT) + getModel().getInnerHTML(child) + getModel().getEndTag(v));
            } else if (child == before) {
                VirtualElement v = getCorrespondingVirtualElement(child);
                buf.append(getModel().getStartTag(v, child) + getModel().getInnerHTML(child) + getModel().getEndTag(v));
            } else if (!getModel().isContent(child))
                if (getModel().isArtificialWithInlineElement(child))
                    buf.append(getModel().getInlineHTML(child));
                else
                    buf.append(changeFocus(child, remove, before, now));
        }

        if (getModel().isLeaf(parent))
            buf.append(getModel().getInnerHTML(parent));
        buf.append(getModel().getEndTag(parent));
        
        return buf.toString();
    }

    /**
     * Modify the element attributes when changing the focus from an old data element to a new data element
     * 
     * @param oldPos
     * @param newPos
     * @throws java.lang.Exception
     */
    public Element moveCaret(int oldPos, int newPos) throws Exception {
        // if oldPos is not within virtual block, ignore it
        Element toRemove = null;
        Element before = getModel().getVirtualSection(oldPos);
        while (cannotHighlight(before)) {
            before = before.getParentElement();
        }
        Element now = getModel().getVirtualSection(newPos);
        while (cannotHighlight(now)) {
            now = now.getParentElement();
        }

        // if focus element has not changed, don't do anything
        if (before == now)
            return before;

        if (getVirtualBlock(before, getModel()) != this)
            before = null;
        else {
            // for the data comparison in hasNewData, we don't need the highlightable element, but the data element
            Element dataBefore = getModel().getDataElement(oldPos);
            VirtualElement posVirtualElement = getCorrespondingVirtualElement(dataBefore);
            if (posVirtualElement != null) {
                boolean forward = newPos > oldPos;
                if (!posVirtualElement.isBinary() && !hasNonDefaultData(dataBefore) && canRemove(posVirtualElement, dataBefore, forward)) {
                    toRemove = getRemovableRoot(dataBefore, posVirtualElement, forward);
                    before = toRemove;
                }
            } else
                before = null;
        }

        // find common parent of the character elements at newPos and oldPos
        Element parent = getModel().getCommonParent(before, now);

        // update the content of the parent: The character element at oldPos shall have no HighlightAttributes.ATT_EDIT, the character element at newPos shall have HighlightAttributes.ATT_EDIT
        String html = changeFocus(parent, toRemove, before, now);

        getModel().setOuterHTML(parent, html);

        focusElement = getModel().getRealParagraph(newPos);
        return focusElement;
    }

    /**
     * Save the state of the component editors, if there are any
     */
    public void saveComponents(CommentedUserRequest comment) {
        List<String> views = new ArrayList<String>(viewRegistry.keySet());
        /* sort keys in lexicographical order. This assumes, that keys for ComponentDialog(s) will
         * be equal to the full data path, i.e. "nodes:testObject/nodes:comment[2]"
         */
        Collections.sort(views);
        Iterator<String> i = views.iterator();
        while (i.hasNext()) {
            String key = i.next();
            Object value = viewRegistry.get(key);
            if (value instanceof ComponentDialogDialog) {
                ComponentDialogDialog dialog = (ComponentDialogDialog) value;
                ((ComponentDialog) dialog.getApplet()).save(comment);
            }
        }
    }
}

